/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.pos.net.toolbox;

import android.annotation.SuppressLint;
import android.os.SystemClock;
import android.text.TextUtils;
import android.webkit.URLUtil;

import com.pos.net.base.AuthFailureError;
import com.pos.net.base.Cache;
import com.pos.net.base.Cache.Entry;
import com.pos.net.base.Network;
import com.pos.net.base.NetworkError;
import com.pos.net.base.NetworkResponse;
import com.pos.net.base.NoConnectionError;
import com.pos.net.base.Request;
import com.pos.net.base.Response.LoadingListener;
import com.pos.net.base.ResponseDelivery;
import com.pos.net.base.RetryPolicy;
import com.pos.net.base.ServerError;
import com.pos.net.base.TimeoutError;
import com.pos.net.base.VolleyError;
import com.pos.net.base.VolleyLog;
import com.pos.net.request.load.SDownloadRequest;
import com.pos.set.Log;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.cookie.DateUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/**
 * A network performing Volley requests over an {@link HttpStack}.
 */
public class BasicNetwork implements Network {
	protected static final boolean DEBUG = VolleyLog.DEBUG;

	private static int SLOW_REQUEST_THRESHOLD_MS = 3000;

	private static int DEFAULT_POOL_SIZE = 4096;

	protected final HttpStack mHttpStack;

	protected final ByteArrayPool mPool;

	/**
	 * @param httpStack
	 *            HTTP stack to be used
	 */
	public BasicNetwork(HttpStack httpStack) {
		// If a pool isn't passed in, then build a small default pool that will
		// give us a lot of
		// benefit and not use too much memory.
		this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
	}

	/**
	 * @param httpStack
	 *            HTTP stack to be used
	 * @param pool
	 *            a buffer pool that improves GC performance in copy operations
	 */
	public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
		mHttpStack = httpStack;
		mPool = pool;
	}

	@SuppressLint("DefaultLocale")
	@Override
	public NetworkResponse performRequest(ResponseDelivery delivery, Request<?> request) throws VolleyError {
		long requestStart = SystemClock.elapsedRealtime();
		while (true) {
			HttpResponse httpResponse = null;
			byte[] responseContents = null;
			Map<String, String> responseHeaders = Collections.emptyMap();

			try {
				Log.e("request.getUrl()", "request.getUrl()="+request.getUrl());
				if (!URLUtil.isNetworkUrl(request.getUrl())) {
					return new NetworkResponse(responseContents);
				}
				// Gather headers.
				Map<String, String> headers = new HashMap<String, String>();
				addCacheHeaders(headers, request.getCacheEntry());

				for (String key : headers.keySet()) {
					if (key.equals("Set-Cookie"))

						Log.e("遍历BasicNetwork中addCacheHeaders Cookie",
								"BasicNetwork key= " + key + " and value= " + headers.get(key));
				}

				

				httpResponse = mHttpStack.performRequest(request, headers);
				StatusLine statusLine = httpResponse.getStatusLine();
				int statusCode = statusLine.getStatusCode();

				responseHeaders = convertHeaders(httpResponse.getAllHeaders());
				for (String key : responseHeaders.keySet()) {
					if (key.equals("Set-Cookie"))
						Log.e("遍历BasicNetwork responseHeaders中Set-Cookie",
								"key= " + key + " and value= " + responseHeaders.get(key));
				}

				// Handle cache validation.
				if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
					Entry entry = request.getCacheEntry();
					if (entry == null) {
						return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, responseHeaders, true);
					}
					// A HTTP 304 response does not have all header fields. We
					// have to use the header fields from the cache entry plus
					// the new ones from the response.
					// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
					entry.responseHeaders.putAll(responseHeaders);
					return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, entry.responseHeaders, true);
				}

				// Handle moved resources
				if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
					String newUrl = responseHeaders.get("Location");
					request.setRedirectUrl(newUrl);
				}

				// Some responses such as 204s do not have content. We must
				// check.
				if (httpResponse.getEntity() != null) {
					if (request instanceof SDownloadRequest) {
						SDownloadRequest downloadRequest = (SDownloadRequest) request;
						// 如果服务器不支持range，不要开启断点下
						if (downloadRequest.isResume() && !isSupportRange(httpResponse)) {
							downloadRequest.setResume(false);
						}
						if (statusCode == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
							return new NetworkResponse(HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE,
									downloadRequest.getTarget().getBytes(), responseHeaders, false);
						} else if (statusCode >= 300) {
							responseContents = entityToBytes(delivery, request, httpResponse.getEntity());
						} else {
							responseContents = handleEntity(delivery, (SDownloadRequest) request,
									httpResponse.getEntity());
						}
					} else {
						responseContents = entityToBytes(delivery, request, httpResponse.getEntity());
					}
				} else {
					// Add 0 byte response as a way of honestly representing a
					// no-content request.
					responseContents = new byte[0];
				}

				// if the request is slow, log it.
				long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
				logSlowRequests(requestLifetime, request, responseContents, statusLine);

				if (statusCode < 200 || statusCode > 299) {
					throw new IOException();
				}
				return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
			} catch (SocketTimeoutException e) {
				attemptRetryOnException("socket", request, new TimeoutError());
			} catch (ConnectTimeoutException e) {
				attemptRetryOnException("connection", request, new TimeoutError());
			} catch (MalformedURLException e) {
				throw new RuntimeException("Bad URL " + request.getUrl(), e);
			} catch (IOException e) {
				e.printStackTrace();
				int statusCode = 0;
				NetworkResponse networkResponse = null;
				if (httpResponse != null) {
					statusCode = httpResponse.getStatusLine().getStatusCode();
				} else {
					throw new NoConnectionError(e);
				}
				if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
					VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl());
				} else {
					VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
				}
				if (responseContents != null) {
					networkResponse = new NetworkResponse(statusCode, responseContents, responseHeaders, false);
					if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
						attemptRetryOnException("auth", request, new AuthFailureError(networkResponse));
					} else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY
							|| statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
						attemptRetryOnException("redirect", request, new AuthFailureError(networkResponse));
					} else {
						// TODO: Only throw ServerError for 5xx status codes.
						throw new ServerError(networkResponse);
					}
				} else {
					throw new NetworkError(networkResponse);
				}
			} finally {
				try {
					if (httpResponse != null && httpResponse.getEntity() != null) {
						httpResponse.getEntity().getContent().close();
					}
				} catch (IllegalStateException e) {
				} catch (IOException e) {
				}
			}
		}
	}

	/**
	 * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
	 */
	private void logSlowRequests(long requestLifetime, Request<?> request, byte[] responseContents,
			StatusLine statusLine) {
		if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
			VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " + "[rc=%d], [retryCount=%s]",
					request, requestLifetime, responseContents != null ? responseContents.length : "null",
					statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
		}
	}

	/**
	 * Attempts to prepare the request for a retry. If there are no more
	 * attempts remaining in the request's retry policy, a timeout exception is
	 * thrown.
	 * 
	 * @param request
	 *            The request to use.
	 */
	private static void attemptRetryOnException(String logPrefix, Request<?> request, VolleyError exception)
			throws VolleyError {
		RetryPolicy retryPolicy = request.getRetryPolicy();
		int oldTimeout = request.getTimeoutMs();

		try {
			retryPolicy.retry(exception);
		} catch (VolleyError e) {
			request.addMarker(String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
			throw e;
		}
		request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
	}

	private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
		// If there's no cache entry, we're done.
		if (entry == null) {
			return;
		}

		if (entry.etag != null) {
			headers.put("If-None-Match", entry.etag);
		}

		if (entry.serverDate > 0) {
			Date refTime = new Date(entry.serverDate);
			headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
		}

	}

	protected void logError(String what, String url, long start) {
		long now = SystemClock.elapsedRealtime();
		VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
	}

	/** Reads the contents of HttpEntity into a byte[]. */
	private byte[] entityToBytes(ResponseDelivery delivery, Request<?> request, HttpEntity entity)
			throws IOException, ServerError {
		PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
		byte[] buffer = null;
		long time = SystemClock.uptimeMillis();
		try {
			InputStream in = entity.getContent();
			if (in == null) {
				throw new ServerError();
			}
			buffer = mPool.getBuf(1024);
			long length = entity.getContentLength();
			long current = 0;
			int count = -1;
			LoadingListener listener = request.getLoadingListener();
			while ((count = in.read(buffer)) != -1) {
				bytes.write(buffer, 0, count);
				current += count;
				if (listener != null) {
					long thisTime = SystemClock.uptimeMillis();
					if (thisTime - time >= request.getRate()) {
						time = thisTime;
						delivery.postLoading(request, length == -1 ? current * 2 : length, current);
					}
				}
			}
			if (listener != null) {
				delivery.postLoading(request, length == -1 ? current : length, current);
			}
			return bytes.toByteArray();
		} finally {
			try {
				// Close the InputStream and release the resources by "consuming
				// the content".
				entity.consumeContent();
			} catch (IOException e) {
				// This can happen if there was an exception above that left the
				// entity in
				// an invalid state.
				VolleyLog.v("Error occured when calling consumingContent");
			}
			mPool.returnBuf(buffer);
			bytes.close();
		}
	}

	private byte[] handleEntity(ResponseDelivery delivery, SDownloadRequest request, HttpEntity entity)
			throws IOException, ServerError {
		long time = SystemClock.uptimeMillis();
		final String target = request.getTarget();
		if (TextUtils.isEmpty(target) || target.trim().length() == 0)
			return null;

		File targetFile = new File(target);

		if (!targetFile.exists()) {
			targetFile.createNewFile();
		}

		if (request.isCanceled()) {
			return target.getBytes();
		}
		FileOutputStream os = null;
		InputStream input = null;
		try {
			long current = 0;
			if (request.isResume()) {
				current = targetFile.length();
				os = new FileOutputStream(target, true);
			} else {
				os = new FileOutputStream(target);
			}

			if (request.isCanceled()) {
				return target.getBytes();
			}

			input = entity.getContent();
			long count = entity.getContentLength() + current;

			if (current >= count || request.isCanceled()) {
				return target.getBytes();
			}
			int readLen = 0;
			byte[] buffer = new byte[1024];
			LoadingListener listener = request.getLoadingListener();
			while (!request.isCanceled() && !(current >= count) && ((readLen = input.read(buffer, 0, 1024)) > 0)) {// δȫ����ȡ
				os.write(buffer, 0, readLen);
				current += readLen;
				if (listener != null) {
					long thisTime = SystemClock.uptimeMillis();
					if (thisTime - time >= request.getRate()) {
						time = thisTime;
						delivery.postLoading(request, count, current);
					}
				}
			}
			if (listener != null) {
				delivery.postLoading(request, count, current);
			}
			if (request.isCanceled() && current < count) { // �û�����ֹͣ
				throw new IOException("user stop download thread");
			}
		} finally {
			if (input != null) {
				input.close();
			}
			try {
				// Close the InputStream and release the resources by "consuming
				// the content".
				entity.consumeContent();
			} catch (IOException e) {
				// This can happen if there was an exception above that left the
				// entity in
				// an invalid state.
				VolleyLog.v("Error occured when calling consumingContent");
			}
			if (os != null) {
				os.close();
			}
		}
		return target.getBytes();
	}

	/**
	 * Converts Headers[] to Map<String, String>.
	 */
	protected static Map<String, String> convertHeaders(Header[] headers) {
		Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
		for (int i = 0; i < headers.length; i++) {
			result.put(headers[i].getName(), headers[i].getValue());
		}
		return result;
	}

	public static String getHeader(HttpResponse response, String key) {
		Header header = response.getFirstHeader(key);
		return header == null ? null : header.getValue();
	}

	public static boolean isSupportRange(HttpResponse response) {
		if (TextUtils.equals(getHeader(response, "Accept-Ranges"), "bytes")) {
			return true;
		}
		String value = getHeader(response, "Content-Range");
		return value != null && value.startsWith("bytes");
	}

	public static boolean isGzipContent(HttpResponse response) {
		return TextUtils.equals(getHeader(response, "Content-Encoding"), "gzip");
	}
}
